package com.google.code.gwtquickstartplugin;

import java.io.File;
import java.util.Map;
import java.util.LinkedHashMap;

import org.apache.maven.plugin.logging.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Parsing helper for <code>&lt;Module&gt;</code>.gwt.xml files
 * given a specific location.
 */
public class GwtModuleToWebXmlMergingHelper {

    private static final RemoteServiceDescription[] EMPTY_REMOTE_SERVICE_DESCRIPTION_ARRAY =
            new RemoteServiceDescription[0];

    private static final ServletDescription[] EMPTY_SERVLET_DESCRIPTION_ARRAY =
            new ServletDescription[0];

    private static final String SRC_MAIN_JAVA_FOLDER = new StringBuffer()
            .append("src").append(File.separator)
            .append("main").append(File.separator).append("java")
            .toString();

    private static final String WEB_XML_PATH = new StringBuffer()
            .append("src").append(File.separator)
            .append("main").append(File.separator)
            .append("webapp").append(File.separator)
            .append("WEB-INF").append(File.separator)
            .append("web.xml").toString();

    private static final String GWT_MODULE_SUFFIX = ".gwt.xml";

    private final Log log;

    private final String gwtModule;

    private final String projectRoot;

    public GwtModuleToWebXmlMergingHelper(Log log, String gwtModule,
                                          String projectRoot) {
        this.log = log;
        this.gwtModule = gwtModule;
        this.projectRoot = projectRoot;
    }

    private String buildGwtModuleFileFullPath(String moduleFullyQualifiedName) {
        String result = moduleFullyQualifiedName;
        result = result.replace('.', File.separator.charAt(0));
        return new StringBuffer(projectRoot)
                .append(File.separator)
                .append(SRC_MAIN_JAVA_FOLDER)
                .append(File.separator)
                .append(result)
                .append(GWT_MODULE_SUFFIX).toString();
    }

    public RemoteServiceDescription[] getGwtModuleServletDescriptions(Log log,
                                                                      String moduleFullyQualifiedName) {
        String fullPath = buildGwtModuleFileFullPath(moduleFullyQualifiedName);
        XmlUtils.verifyFileExists(log, fullPath);
        Document doc = XmlUtils.parseXmlFileToDom(log, fullPath);
        return (RemoteServiceDescription[]) XmlUtils
                .parseForElements(log, doc, "servlet",
                        EMPTY_REMOTE_SERVICE_DESCRIPTION_ARRAY,
                        new ElementMapper() {
                            public Object[] buildArrayForResults(
                                    int elementCount) {
                                return new RemoteServiceDescription[elementCount];
                            }

                            public Object mapElement(Log log, Element element) {
                                RemoteServiceDescription description =
                                        new RemoteServiceDescription();
                                description
                                        .setPath(element.getAttribute("path"));
                                description.setModuleFqn(
                                        element.getAttribute("class"));
                                return description;

                            }
                        }
                );
    }

    private Map buildServletsByNameMap(Log log, ServletDescription[] servlets) {
        Map result = new LinkedHashMap();
        for (int i = 0; i < servlets.length; i++) {
            if (result.get(servlets[i].getName()) != null) {
                log.warn("A servlet named " + servlets[i].getName() +
                        " was already processed. Check your web_.xml file for multiple definitions.");
            }
            result.put(servlets[i].getName(), servlets[i]);
        }
        return result;
    }

    private ServletDescription[] getServletDescriptions(Log log,
                                                        Document webXmlDoc) {
        return (ServletDescription[])
                XmlUtils.parseForElements(log, webXmlDoc, "servlet",
                        EMPTY_SERVLET_DESCRIPTION_ARRAY,
                        new ElementMapper() {
                            public Object[] buildArrayForResults(
                                    int elementCount) {
                                return new ServletDescription[elementCount];
                            }

                            public Object mapElement(Log log, Element element) {
                                ServletDescription description =
                                        new ServletDescription();
                                description.setName(XmlUtils.getTextValue(
                                        element, "servlet-name"));
                                description.setClassName(
                                        XmlUtils.getTextValue(element,
                                                "servlet-class"));
                                return description;

                            }
                        }
                );
    }

    private void completeServletDescriptions(Log log, Document webXmlDoc,
                                             ServletDescription[] servletDescriptions) {
        final Map servletsByName =
                buildServletsByNameMap(log, servletDescriptions);
        XmlUtils.parseForElements(log, webXmlDoc, "servlet-mapping",
                EMPTY_SERVLET_DESCRIPTION_ARRAY,
                new ElementMapper() {
                    public Object[] buildArrayForResults(
                            int elementCount) {
                        return new ServletDescription[elementCount];
                    }

                    public Object mapElement(Log log, Element element) {
                        String servletName = XmlUtils.getTextValue(
                                element, "servlet-name");
                        ServletDescription description =
                                (ServletDescription) servletsByName
                                        .get(servletName);
                        if (description == null) {
                            log.warn("Servlet [" + servletName +
                                    "] has a mapping and no corresponding <servlet> element");
                        } else {
                            description.setUrlPattern(
                                    XmlUtils.getTextValue(element,
                                            "url-pattern"));

                        }
                        return description;
                    }
                }
        );

    }

    private String buildWebXmlFileFullPath(String webXmlRelativePath) {
        return new StringBuffer(projectRoot)
                .append(File.separator)
                .append(webXmlRelativePath)
                .toString();
    }

    public ServletDescription[] getWebXmlServletDescriptions(Log log) {
        String fullPath = buildWebXmlFileFullPath(WEB_XML_PATH);
        XmlUtils.verifyFileExists(log, fullPath);
        Document webXmlDoc = XmlUtils.parseXmlFileToDom(log, fullPath);
        ServletDescription[] servletDescriptions =
                getServletDescriptions(log, webXmlDoc);
        completeServletDescriptions(log, webXmlDoc, servletDescriptions);
        return servletDescriptions;
    }

    private String buildServletKey(Servlet servlet) {
        return new StringBuffer()
                .append(servlet.getUrl())
                .append("|$|")
                .append(servlet.getClassName())
                .toString();
    }

    private Map buildServletMapByUrlAndClassName(
            ServletDescription[] servlets) {
        Map servletMap = new LinkedHashMap();

        for (int i = 0; i < servlets.length; i++) {
            servletMap.put(buildServletKey(servlets[i]),
                    servlets[i]);
        }

        return servletMap;
    }

    private ServletDescription[] discoverServletDefinitionsToMerge(
            RemoteServiceDescription[] gwtServlets,
            ServletDescription[] webXmlServlets) {
        Map toMerge = buildServletMapByUrlAndClassName(webXmlServlets);

        for (int i = 0; i < gwtServlets.length; i++) {
            ServletDescription webXmlDescription =
                    (ServletDescription) toMerge
                            .get(buildServletKey(gwtServlets[i]));
            if (webXmlDescription == null) {
                // No web_.xml servlet found, add a new one
                ServletDescription newDescription = new ServletDescription();
                newDescription.setName(gwtServlets[i].getModuleName());
                newDescription.setUrlPattern(gwtServlets[i].getPath());
                newDescription.setClassName(gwtServlets[i].getClassName());
                toMerge.put(buildServletKey(gwtServlets[i]), newDescription);
            }
        }

        ServletDescription[] servlets =
                new ServletDescription[toMerge.values().size()];
        return (ServletDescription[]) toMerge.values().toArray(servlets);
    }

    public Element[] buildServletElements(Document doc,
                                          ServletDescription[] servlets) {
        Element[] servletElements = new Element[servlets.length];
        for (int i = 0; i < servlets.length; i++) {
            servletElements[i] = buildServletElement(doc, servlets[i]);
        }
        return servletElements;
    }

    private Element buildServletElement(Document doc,
                                        ServletDescription servlet) {
        Element servletElement = doc.createElement("servlet");
        servletElement.appendChild(XmlUtils.createElementWithText(doc,
                "servlet-name", servlet.getName()));
        servletElement.appendChild(XmlUtils.createElementWithText(doc,
                "servlet-class", servlet.getClassName()));
        return servletElement;
    }

    public Element[] buildServletMappingElements(Document doc,
                                                 ServletDescription[] servlets) {
        Element[] servletMappingElements = new Element[servlets.length];
        for (int i = 0; i < servlets.length; i++) {
            servletMappingElements[i] =
                    buildServletMappingElement(doc, servlets[i]);
        }
        return servletMappingElements;
    }

    private Element buildServletMappingElement(Document doc,
                                               ServletDescription servlet) {
        Element servletMappingElement = doc.createElement("servlet-mapping");
        servletMappingElement.appendChild(XmlUtils.createElementWithText(doc,
                "servlet-name", servlet.getName()));
        servletMappingElement.appendChild(XmlUtils.createElementWithText(doc,
                "url-pattern",
                "/" + gwtModule + servlet.getUrlPattern()));
        return servletMappingElement;
    }

    private String buildFullMergedWebXmlPath(String webappFolderName) {
        return new StringBuffer()
                .append(webappFolderName)
                .append(File.separator)
                .append("WEB-INF")
                .append(File.separator)
                .append("web.xml")
                .toString();
    }


    public void mergeGwtModuleToWebXml(
            String webappName,
            String moduleFullyQualifiedName) {
        RemoteServiceDescription[] gwtServlets =
                getGwtModuleServletDescriptions(log, moduleFullyQualifiedName);
        log.info(gwtServlets.length + " servlets found in GWT Module.");

        ServletDescription[] webXmlServlets =
                getWebXmlServletDescriptions(log);
        log.info(webXmlServlets.length + " servlets found in web.xml");

        ServletDescription[] toMerge =
                discoverServletDefinitionsToMerge(gwtServlets, webXmlServlets);

        Document webXml = XmlUtils.parseXmlFileToDom(log,
                projectRoot + File.separator + WEB_XML_PATH);
        XmlUtils.addElements(log, webXml.getDocumentElement(),
                buildServletElements(webXml, toMerge), false);
        XmlUtils.addElements(log, webXml.getDocumentElement(),
                buildServletMappingElements(webXml, toMerge), false);
        XmlUtils.serializeDocument(buildFullMergedWebXmlPath(webappName),
                webXml);
    }

}
